package com.wcs.exam.common.faceid;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.google.common.hash.Hashing;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.faceid.v20180301.FaceidClient;
import com.tencentcloudapi.faceid.v20180301.models.*;
import com.tencentcloudapi.iai.v20200303.IaiClient;
import com.tencentcloudapi.iai.v20200303.models.CompareFaceRequest;
import com.tencentcloudapi.iai.v20200303.models.CompareFaceResponse;
import com.wcs.exam.common.faceid.bean.FaceQueryRecordResult;
import com.wcs.exam.common.faceid.bean.GetfaceidReturn;
import com.wcs.exam.common.faceid.bean.NonceTicket;
import com.wcs.exam.common.util.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.HashedMap;
import org.springframework.util.StringUtils;

import javax.net.ssl.HttpsURLConnection;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 腾讯人脸识别工具类
 */
@Slf4j
public final class FaceContrastUtil {

    private FaceContrastUtil() {
    }

    /**
     * 实名核身鉴权
     * 参考文档：<a href="https://cloud.tencent.com/document/product/1007/31816">实名核身鉴权</a>
     * 参考文档：<a href="https://cloud.tencent.com/document/product/1007/103091">微信普通 H5 配置流程</a>
     *
     * @param secretId    腾讯人脸秘钥ID
     * @param secretKey   腾讯人脸秘钥
     * @param ruleId      角色ID，用于细分客户使用场景
     * @param name        姓名
     * @param idCard      身份标识
     * @param imageBase64 照片对比时必传，用于人脸比对的照片，图片的Base64值；Base64编码后的图片数据大小不超过3M，仅支持jpg、png格式。请使用标准的Base64编码方式(带=补位)，编码规范参考RFC4648。
     * @param redirectUrl 认证结束后重定向的回调链接地址。最长长度1024位。
     * @param extra       透传字段，在获取验证结果时返回。
     * @return 实名核身鉴权跳转地址信息
     */
    public static DetectAuthResponse detectAuth(String secretId, String secretKey, String ruleId, String name, String idCard, String imageBase64, String redirectUrl, String extra) {
        DetectAuthRequest req = new DetectAuthRequest();
        req.setRuleId(ruleId);
        req.setName(name);
        req.setIdCard(idCard);
        req.setRedirectUrl(redirectUrl);
        if (StrUtil.isNotBlank(imageBase64)) {
            req.setImageBase64(imageBase64);
        }
        if (StrUtil.isNotBlank(extra)) {
            req.setExtra(extra);
        }

        Credential credential = new Credential(secretId, secretKey);
        FaceidClient client = new FaceidClient(credential, "ap-guangzhou");
        try {
            return client.DetectAuth(req);
        } catch (TencentCloudSDKException e) {
            log.error("实名核身鉴权失败！", e);
            return null;
        }
    }

    /**
     * 获取实名核身结果信息增强版
     * 参考文档：<a href="https://cloud.tencent.com/document/product/1007/41957">获取实名核身结果信息增强版</a>
     * 参考文档：<a href="https://cloud.tencent.com/document/product/1007/103091">微信普通 H5 配置流程</a>
     *
     * @param secretId  腾讯人脸秘钥ID
     * @param secretKey 腾讯人脸秘钥
     * @param ruleId    角色ID，用于细分客户使用场景
     * @param bizToken  人脸核身流程的标识，调用DetectAuth接口时生成
     * @return 获取实名核身结果信息增强版响应
     */
    public static GetDetectInfoEnhancedResponse getDetectInfoEnhanced(String secretId, String secretKey, String ruleId, String bizToken) {
        GetDetectInfoEnhancedRequest req = new GetDetectInfoEnhancedRequest();
        req.setRuleId(ruleId);
        req.setBizToken(bizToken);
        req.setBestFramesCount(1L);

        Credential credential = new Credential(secretId, secretKey);
        FaceidClient client = new FaceidClient(credential, "ap-guangzhou");
        try {
            return client.GetDetectInfoEnhanced(req);
        } catch (TencentCloudSDKException e) {
            log.error("实名核身鉴权失败！", e);
            return null;
        }
    }

    /**
     * 活体人脸比对：传入视频和照片，先判断视频中是否为真人，判断为真人后，再判断该视频中的人与上传照片是否属于同一个人
     * <a href="https://cloud.tencent.com/document/product/1007/31819">参考文档</a>
     *
     * @param secretId    腾讯人脸秘钥ID
     * @param secretKey   腾讯人脸秘钥
     * @param imageBase64 把用户账号中已存的人脸对比图加密成Base64编码
     * @param videoBase64 前端传来的Base64视频
     * @return 活体人脸比对结果
     */
    public static LivenessCompareResponse livenessCompare(String secretId, String secretKey, String imageBase64, String videoBase64) {
        try {
            Credential credential = new Credential(secretId, secretKey);
            FaceidClient client = new FaceidClient(credential, "ap-guangzhou");

            LivenessCompareRequest request = new LivenessCompareRequest();
            // 用于人脸比对的照片，图片的BASE64值；BASE64编码后的图片数据大小不超过3M，仅支持jpg、png格式
            request.setImageBase64(imageBase64);
            // 用于活体检测的视频，视频的BASE64值；BASE64编码后的大小不超过8M，支持mp4、avi、flv格式
            request.setVideoBase64(videoBase64);
            // 活体检测类型，LIP为数字模式，ACTION为动作模式，SILENT为静默模式
            request.setLivenessType("SILENT");

            // 额外配置，传入JSON字符串。{"BestFrameNum": 2 //需要返回多张最佳截图，取值范围1-10 }
            JSONObject optionalJson = new JSONObject();
            optionalJson.set("BestFrameNum", 1);
            request.setOptional(optionalJson.toString());

            return client.LivenessCompare(request);
        } catch (TencentCloudSDKException e) {
            log.error(e.toString());
        }
        return null;
    }

    /**
     * 活体人脸核身: 传入视频和身份信息，先判断视频中是否为真人，判断为真人后，再判断该视频中的人与公安权威库的证件照是否属于同一个人
     * <a href="https://cloud.tencent.com/document/product/1007/31818">...</a>
     *
     * @param secretId    腾讯人脸秘钥ID
     * @param secretKey   腾讯人脸秘钥
     * @param idCard      身份证号
     * @param name        用户真实姓名
     * @param videoBase64 前端传来的Base64视频
     * @return 活体人脸核身结果
     */
    public static LivenessRecognitionResponse livenessRecognition(String secretId, String secretKey, String idCard, String name, String videoBase64) {
        try {
            Credential credential = new Credential(secretId, secretKey);
            FaceidClient client = new FaceidClient(credential, "ap-guangzhou");

            LivenessRecognitionRequest req = new LivenessRecognitionRequest();
            // 身份证号
            req.setIdCard(idCard);
            // 姓名
            req.setName(name);
            // 用于活体检测的视频，视频的BASE64值；BASE64编码后的大小不超过8M，支持mp4、avi、flv格式
            req.setVideoBase64(videoBase64);
            // 活体检测类型，LIP为数字模式，ACTION为动作模式，SILENT为静默模式
            req.setLivenessType("SILENT");

            // 额外配置，传入JSON字符串。{"BestFrameNum": 2 //需要返回多张最佳截图，取值范围1-10 }
            JSONObject optionalJson = new JSONObject();
            optionalJson.set("BestFrameNum", 1);
            req.setOptional(optionalJson.toString());


            return client.LivenessRecognition(req);
        } catch (TencentCloudSDKException e) {
            log.error("活体人脸核身失败", e);
        }
        return null;
    }

    /**
     * 人脸比对: 对两张图片中的人脸进行相似度比对，返回人脸相似度分数。
     * <a href="https://cloud.tencent.com/document/product/867/32802">...</a>
     *
     * @param secretId  腾讯人脸秘钥ID
     * @param secretKey 腾讯人脸秘钥
     * @param imageA    A 图片
     * @param imageB    B 图片
     * @return 人脸比对结果
     */
    public static CompareFaceResponse compareFace(String secretId, String secretKey, String imageA, String imageB) {
        try {
            Credential credential = new Credential(secretId, secretKey);

            IaiClient client = new IaiClient(credential, "ap-guangzhou");

            CompareFaceRequest req = new CompareFaceRequest();

            // A 图片 base64 数据，base64 编码后大小不可超过5M。
            //若图片中包含多张人脸，只选取其中人脸面积最大的人脸。
            //支持PNG、JPG、JPEG、BMP，不支持 GIF 图片。
            req.setImageA(imageA);

            // B 图片 base64 数据，base64 编码后大小不可超过5M。
            //若图片中包含多张人脸，只选取其中人脸面积最大的人脸。
            //支持PNG、JPG、JPEG、BMP，不支持 GIF 图片。
            req.setImageB(imageB);
            req.setFaceModelVersion("3.0");


            // 图片质量控制。
            //0: 不进行控制；
            //1:较低的质量要求，图像存在非常模糊，眼睛鼻子嘴巴遮挡至少其中一种或多种的情况；
            //2: 一般的质量要求，图像存在偏亮，偏暗，模糊或一般模糊，眉毛遮挡，脸颊遮挡，下巴遮挡，至少其中三种的情况；
            //3: 较高的质量要求，图像存在偏亮，偏暗，一般模糊，眉毛遮挡，脸颊遮挡，下巴遮挡，其中一到两种的情况；
            //4: 很高的质量要求，各个维度均为最好或最多在某一维度上存在轻微问题；
            //默认 0。
            //若图片质量不满足要求，则返回结果中会提示图片质量检测不符要求。
            //示例值：0
            req.setQualityControl(0L);


            return client.CompareFace(req);
        } catch (TencentCloudSDKException e) {
            log.error("人脸比对失败", e);
        }
        return null;
    }

    /**
     * 获取 Access Token
     *
     * @param wbappid 业务流程唯一标识，即 wbappid，可参考 获取 WBappid 指引在人脸核身控制台内申请
     * @param secret  wbappid 对应的密钥，申请 wbappid 时得到，可参考 获取 WBappid 指引在人脸核身控制台内申请
     * @return Access Token
     */
    public static String accessToken(String wbappid, String secret) {
        String accessTokenUrl = "https://idasc.webank.com/api/oauth2/access_token";
        Map<String, Object> map = new HashedMap<>();
        map.put("app_id", wbappid);
        map.put("secret", secret);
        map.put("grant_type", "client_credential");
        map.put("version", "1.0.0");
        JSONObject accessTokenObject = HttpUtils.getForObject(accessTokenUrl, map, JSONObject.class);
        if (!"0".equals(accessTokenObject.get("code"))) {
            log.error("app获取人脸识别sign失败={}", accessTokenObject.get("code"));
            return null;
        }
        return accessTokenObject.getStr("access_token");
    }

    /**
     * @param wbappid     业务流程唯一标识，即 wbappid，可参考 获取 WBappid 指引在人脸核身控制台内申请
     * @param type        ticket 类型，默认值：SIGN（必须大写）
     * @param userId      请根据 获取 Access Token 指引进行获取
     * @param accessToken 请根据 获取 Access Token 指引进行获取
     * @return ticket
     */
    public static String apiTicket(String wbappid, String type, String userId, String accessToken) {
        String ticketUrl = "https://idasc.webank.com/api/oauth2/api_ticket";
        Map<String, Object> ticketMap = new HashedMap<>();
        ticketMap.put("app_id", wbappid);
        ticketMap.put("access_token", accessToken);
        ticketMap.put("type", type);
        ticketMap.put("version", "1.0.0");
        if (StringUtils.hasText(userId)) {
            ticketMap.put("user_id", userId);
        }
        JSONObject ticketMapObject = HttpUtils.getForObject(ticketUrl, ticketMap, JSONObject.class);
        if (!"0".equals(ticketMapObject.get("code"))) {
            log.error("app获取人脸识别SIGN ticket失败={}", ticketMapObject);
            return null;
        }
        NonceTicket nonceTicket = ticketMapObject.getJSONArray("tickets").get(0, NonceTicket.class);
        return nonceTicket.getValue();
    }


    /**
     * 合作方后台上送身份信息(App用到)
     * <a href="https://cloud.tencent.com/document/product/1007/35866">参考文档</a>
     *
     * @param webankAppId     业务流程唯一标识，即 wbappid，可参考 获取 WBappid 指引在人脸核身控制台内申请
     * @param orderNo         订单号，由合作方上送，每次唯一，不能超过32位
     * @param name            姓名
     * @param idNo            证件号码
     * @param userId          用户 ID ，用户的唯一标识（不能带有特殊字符），需要跟生成签名的 userId 保持一致
     * @param sourcePhotoStr  比对源照片(非必填)
     * @param sourcePhotoType 比对源照片类型：1、水纹正脸照；2、高清正脸照(非必填)
     * @param sign            签名：使用上面生成的签名
     * @param nonce           随机数
     * @return 合作方后台上送身份信息
     */
    public static com.wcs.exam.common.faceid.bean.GetfaceidReturn getFaceId(String webankAppId, String orderNo, String name, String idNo, String userId, String sourcePhotoStr, String sourcePhotoType, String sign, String nonce) {
        String ticketUrl = "https://idasc.webank.com/api/server/getfaceid";
        try {
            Map<String, Object> ticketMap = new HashedMap<>();
            ticketMap.put("webankAppId", webankAppId);
            ticketMap.put("orderNo", orderNo);
            ticketMap.put("name", name);
            ticketMap.put("idNo", idNo);
            ticketMap.put("userId", userId);
            if (StringUtils.hasText(sourcePhotoStr)) {
                ticketMap.put("sourcePhotoStr", sourcePhotoStr);
            }
            if (StringUtils.hasText(sourcePhotoType)) {
                ticketMap.put("sourcePhotoType", sourcePhotoType);
            }
            ticketMap.put("sign", sign);
            ticketMap.put("version", "1.0.0");
            ticketMap.put("nonce", nonce);
            JSONObject jsonObject = JSONUtil.parseObj(HttpUtil.post(ticketUrl, JSONUtil.toJsonStr(ticketMap)));
            return jsonObject.toBean(GetfaceidReturn.class);
        } catch (Exception e) {
            log.error("活体人脸核身失败", e);
        }
        return null;

    }

    /**
     * 查询核身结果
     *
     * @param appId       腾讯云控制台申请的 appid
     * @param appIdSecret 腾讯云控制台申请的 appIdSecret
     * @param orderNo     人脸核身订单号
     * @param getFile     是否需要获取人脸识别的视频和文件，值为1则返回视频和照片、值为2则返回照片、值为3则返回视频；其他则不返回
     * @return 核身结果
     * @see <a href="https://cloud.tencent.com/document/product/1007/35880">查询核身结果</a>
     */
    public static FaceQueryRecordResult queryFaceRecord(String appId, String appIdSecret, String orderNo, String getFile) {
        String requestUrl = String.format("https://kyc1.qcloud.com/api/v2/base/queryfacerecord?orderNo=%s", orderNo);

        String accessToken = FaceContrastUtil.accessToken(appId, appIdSecret);
        String apiTicket = FaceContrastUtil.apiTicket(appId, "SIGN", null, accessToken);
        String nonce = IdUtil.simpleUUID();

        List<String> values = new ArrayList<>();
        values.add(appId);
        values.add(orderNo);
        values.add("1.0.0");
        values.add(nonce);
        String sign = FaceContrastUtil.sign(values, apiTicket);
        if (StrUtil.isBlank(sign)) {
            return null;
        }

        JSONObject paramJson = new JSONObject();
        paramJson.set("appId", appId)
                .set("version", "1.0.0")
                .set("nonce", nonce)
                .set("orderNo", orderNo)
                .set("sign", sign);
        if (StrUtil.isNotBlank(getFile)) {
            paramJson.set("getFile", getFile);
        }
        paramJson.set("queryVersion", "1.0");

        String result = HttpUtil.post(requestUrl, paramJson.toString(), 10000);
        JSONObject jsonResult = JSONUtil.parseObj(result);
        if (log.isInfoEnabled()) {
            log.info("人脸核身查询结果{}", jsonResult);
        }
        if (ObjectUtil.isNull(jsonResult)) {
            return null;
        }

        FaceQueryRecordResult recordResult;
        String resultStr = jsonResult.getStr("result");
        if (StrUtil.isNotBlank(resultStr)) {
            recordResult = JSONUtil.toBean(resultStr, FaceQueryRecordResult.class);
        } else {
            recordResult = new FaceQueryRecordResult();
        }

        String code = jsonResult.getStr("code");
        recordResult.setCode(code);

        String bizSeqNo = jsonResult.getStr("bizSeqNo");
        recordResult.setBizSeqNo(bizSeqNo);
        return recordResult;
    }

    public static String sign(String wbAppid, String userId, String nonce, String ticket) {
        List<String> value = Arrays.asList(wbAppid, userId, nonce, "1.0.0");
        return sign(value, ticket);
    }

    public static String sign(List<String> values, String ticket) {
        if (values == null) {
            throw new NullPointerException("values is null");
        }
        values.removeAll(Collections.singleton(null));
        values.add(ticket);
        Collections.sort(values);
        StringBuilder sb = new StringBuilder();
        for (String s : values) {
            sb.append(s);
        }

        return Hashing.sha1().hashString(sb, StandardCharsets.UTF_8).toString().toUpperCase();
    }


    public static String encodeBase64File(String imgUrl) {
        ByteArrayOutputStream outPut = new ByteArrayOutputStream();
        byte[] data = new byte[1024];
        try {
            // 创建URL
            URL url = URI.create(imgUrl).toURL();
            // 创建链接
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(10 * 1000);

            if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
                //连接失败/链接失效/图片不存在
                return "fail";
            }
            InputStream inStream = conn.getInputStream();
            int len;
            while ((len = inStream.read(data)) != -1) {
                outPut.write(data, 0, len);
            }
            inStream.close();
        } catch (IOException e) {
            log.error("获取文件内容失败\n", e);
            return "fail";
        }
        // 对字节数组Base64编码
        return Base64.getEncoder().encodeToString(outPut.toByteArray());
    }
}
